
import applyMixin from './mixin'
import ModuleCollection from './module/module-collection'

let Vue

export function install (_Vue) {
  Vue = _Vue
  applyMixin(Vue)
}

export class Store {

  #actions = Object.create(null)
  #mutations = Object.create(null)
  #wrappedGetters = Object.create(null)
  #modules
  #modulesNamespaceMap = Object.create(null)
  #makeLocalGettersCache = Object.create(null)
  #vm
  get state() {
    return this.#vm._data.$$state
  }
  getters = Object.create(null)

  constructor (options) {
    this.#modules = new ModuleCollection(options)
    Store.#installModule(this, this.#modules.root.state, [], this.#modules.root)
    Store.#resetStoreVM(this, this.#modules.root.state)
  }

  commit = (type, payload) => {

    const entry = this.#mutations[type]
    if (!entry) {
      console.error(`[vuex] unknown mutation type: ${type}`)
      return
    }
    entry.forEach((handler) => {
      handler(payload)
    })
  }

  dispatch = (type, payload) => {
    const entry = this.#actions[type]
    if (!entry) {
      console.error(`[vuex] unknown action type: ${type}`)
      return
    }

    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    return new Promise((resolve, reject) => {
      result.then(resolve, reject)
    })
  }

  registerModule (path, rawModule) {
    if (typeof path === 'string') path = [path]
    this.#modules.register(path, rawModule)
    Store.#installModule(this, this.state, path, this.#modules.get(path))
    // reset store to update getters...
    Store.#resetStoreVM(this, this.state)
  }

  static #installModule (store, rootState, path, module) {
    const isRoot = path.length === 0
    const namespace = store.#modules.getNamespace(path)

    if (module.namespaced) {
      store.#modulesNamespaceMap[namespace] = module
    }

    if (!isRoot) {
      const parentState = this.#getNestedState(rootState, path.slice(0, -1))
      const moduleName = path[path.length - 1]
      parentState[moduleName] = module.state
    }

    const ctx = this.#makeLocalContext(store, namespace, path)

    module.forEachMutation((name, handler) => {
      this.#registerMutation(store, namespace + name, handler, ctx)
    })

    module.forEachAction((name, handler) => {
      this.#registerAction(store, namespace + name, handler, ctx)
    })

    module.forEachGetter((name, getter) => {
      this.#registerGetter(store, namespace + name, getter, ctx)
    })

    module.forEachChild((name, child) => {
      this.#installModule(store, rootState, [...path, name], child)
    })

  }

  static #resetStoreVM (store, state) {
    store.getters = Object.create(null)
    store.#makeLocalGettersCache = Object.create(null)
    const computed = {}
    for (const [key, wrappedGetter] of Object.entries(store.#wrappedGetters)) {
      computed[key] = () => wrappedGetter(store)

      Object.defineProperty(store.getters, key, {
        get: () => store.#vm[key],
        enumerable: true // for local getters
      })
    }
    store.#vm = new Vue({
      data: {
        $$state: state
      },
      computed
    })
  }

  static #getNestedState (state, path) {
    return path.reduce((state, key) => state[key], state)
  }

  static #makeLocalContext (store, namespace, path) {
    const noNamespace = namespace === ''
    const ctx = {
      dispatch: noNamespace ? store.dispatch : (_type, payload, options) => {
        const type = _type
        if (!options?.root) {
          type = namespace + type
          if (!store.#actions[type]) {
            console.error(`[vuex] unknown local action type: ${_type}, global type: ${type}`)
            return
          }
        }
        return store.dispatch(type, payload)
      },
      commit: noNamespace ? store.commit : (_type, payload, options) => {
        let type = _type
        if (!options?.root) {
          type = namespace + type
          if (!store.#mutations[type]) {
            console.error(`[vuex] unknown local mutation type: ${_type}, global type: ${type}`)
            return
          }
        }
        store.commit(type, payload)
      }
    }
    Object.defineProperties(ctx, {
      getters: {
        get: noNamespace
          ? () => store.getters
          : () => this.#makeLocalGetters(store, namespace)
      },
      state: {
        get: () => this.#getNestedState(store.state, path)
      }
    })
    return ctx
  }

  static #registerMutation (store, type, handler, ctx) {
    let entry = store.#mutations[type]
    if (!entry) {
      entry = store.#mutations[type] = []
    }
    entry.push(function wrappedMutationHandler (payload) {
      handler.call(store, ctx.state, payload)
    })
  }

  static #registerAction (store, type, handler, ctx) {
    let entry = store.#actions[type]
    if (!entry) {
      entry = store.#actions[type] = []
    }
    entry.push(function wrappedActionHandler (payload) {
      let result = handler.call(store, {
        dispatch: ctx.dispatch,
        commit: ctx.commit,
        getters: ctx.getters,
        state: ctx.state,
        rootGetters: store.getters,
        rootState: store.state
      }, payload)
      if (typeof result?.then !== 'function') {
        result = Promise.resolve(result)
      }
      return result
    })
  }

  static #registerGetter (store, type, rawGetter, ctx) {
    if (store.#wrappedGetters[type]) {
      console.error(`[vuex] duplicate getter key: ${type}`)
      return
    }
    store.#wrappedGetters[type] = function wrappedGetter (store) {
      return rawGetter(ctx.state, ctx.getters, store.state, store.getters)
    }
  }

  static #makeLocalGetters (store, namespace) {
    if (!store.#makeLocalGettersCache[namespace]) {
      const gettersProxy = {}
      const splitPos = namespace.length
      Object.keys(store.getters).forEach(type => {
        if (type.slice(0, splitPos) !== namespace) return
        const localType = type.slice(splitPos)
        Object.defineProperty(gettersProxy, localType, {
          get: () => store.getters[type],
          enumerable: true
        })
      })
      store.#makeLocalGettersCache[namespace] = gettersProxy
    }
  
    return store.#makeLocalGettersCache[namespace]
  }

}


